package com.cjx.minio.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.cjx.common.exception.Asserts;
import com.cjx.minio.api.FileManagementService;
import com.cjx.minio.constant.FileManagementConstant;
import com.cjx.minio.dao.FileManagementDao;
import com.cjx.minio.entity.FileManagementEntity;
import com.cjx.minio.dto.BucketPolicyConfigDto;
import com.cjx.minio.dto.FileManagementDTO;
import com.cjx.minio.util.FileUtils;
import com.cjx.minio.util.MediaUtil;
import com.cjx.minio.util.VideoConvertUtil;
import com.cjx.minio.websocket.WebSocketServer;
import io.minio.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import ws.schild.jave.Encoder;
import ws.schild.jave.EncodingAttributes;
import ws.schild.jave.MultimediaObject;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.FileNameMap;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 *@Author cjunxian
 * @version 1.0.0
 * @date 2020/09/24 11:24:14
 * @doc 文件管理服务实现类
 */
@Slf4j
@Service
public class FileManagementServiceImpl implements FileManagementService {

  @Autowired
  private FileManagementDao fileManagementDao;

  @Autowired
  private MinioClient minioClient;

  @Value("${minio.endpoint}")
  private String ENDPOINT;

  @Value("${minio.bucketName}")
  private String BUCKET_NAME;

  @Override
  public FileManagementDTO getFileManagementById(String fileId) {
    FileManagementDTO fileManagementDTO =
            BeanUtil.toBean(fileManagementDao.findById(fileId).orElse(null), FileManagementDTO.class);
    if (FileManagementConstant.NEED_PREVIEW.equals(fileManagementDTO.getIsNeedPreview())) {
      fileManagementDTO.setPreviewPath(
              ENDPOINT + "/" + BUCKET_NAME + "/" + fileManagementDTO.getRelativePath());
    } else {
      fileManagementDTO.setDownloadPath(
              ENDPOINT + "/" + BUCKET_NAME + "/" + fileManagementDTO.getRelativePath());
    }
    return fileManagementDTO;
  }

  @Override
  public List<FileManagementDTO> getFileManagementByIds(List<String> fileIds) {
    List<FileManagementEntity> fileManagementEntityList = fileManagementDao.findAllById(fileIds);
    if (fileManagementEntityList == null||fileManagementEntityList.get(0) == null) {
      Asserts.fail("操作失败,获取列表数据为空");
    }
    List<FileManagementDTO> fileManagementDTOList = new ArrayList<>();
    for (FileManagementEntity entity : fileManagementEntityList) {
      FileManagementDTO fileManagementDTO = BeanUtil.toBean(entity, FileManagementDTO.class);
      if (FileManagementConstant.NEED_PREVIEW.equals(fileManagementDTO.getIsNeedPreview())) {
        fileManagementDTO.setPreviewPath(
                ENDPOINT + "/" + BUCKET_NAME + "/" + fileManagementDTO.getRelativePath());
      } else {
        fileManagementDTO.setDownloadPath(
                ENDPOINT + "/" + BUCKET_NAME + "/" + fileManagementDTO.getRelativePath());
      }
      fileManagementDTOList.add(fileManagementDTO);
    }
    return fileManagementDTOList;
  }

  @Override
  public boolean deleteFileManagementById(String fileId) {
    // 1.根据id查询文件信息
    FileManagementEntity entity = fileManagementDao.findById(fileId).orElse(null);
    if (entity == null) {
      Asserts.fail("文件不存在,请稍后重试");
    }
    fileManagementDao.delete(entity);
    try {
      minioClient.removeObject(
              RemoveObjectArgs.builder().bucket(BUCKET_NAME).object(entity.getRelativePath()).build());
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      log.info("删除时发生错误: {}！", e.getMessage());
    }
    return false;
  }

  @Override
  public boolean deleteFileManagementByIds(List<String> fileIds) {
    for (String fileId : fileIds) {
      this.deleteFileManagementById(fileId);
    }
    return true;
  }

  @Override
  public FileManagementDTO saveFile(MultipartFile file, String userId, Long canPreview) {
    try {
      return this.saveFile(file.getOriginalFilename(),file.getSize(),file.getContentType(),file.getInputStream(),userId,canPreview);
    } catch (IOException e) {
      e.printStackTrace();
      Asserts.fail("文件上传失败");
      return null;
    }
  }

  @Override
  public FileManagementDTO saveFile(String originalFilename, Long size, String contentType, InputStream inputStream, String userId, Long canPreview) {
    checkMinioStorageBucket();
    FileInputStream fs = null;
    try {
      FileManagementDTO fileManagementDTO = this.saveFileManagement(originalFilename, size, userId, canPreview);
      // 使用putObject上传一个文件到存储桶中
      PutObjectArgs putObjectArgs = PutObjectArgs.builder()
              .bucket(BUCKET_NAME)
              .object(fileManagementDTO.getRelativePath())
              .contentType(contentType)
              .stream(inputStream, size, ObjectWriteArgs.MIN_MULTIPART_SIZE)
              .build();
      minioClient.putObject(putObjectArgs);

      // 凭借上传之后的回显信息,判断是否可以预览,预览则返回预览地址,不可就返回下载地址
      if (FileManagementConstant.NEED_PREVIEW.equals(canPreview)) {
        fileManagementDTO.setPreviewPath(
                ENDPOINT + "/" + BUCKET_NAME + "/" + fileManagementDTO.getRelativePath());
      } else {
        fileManagementDTO.setDownloadPath(
                ENDPOINT + "/" + BUCKET_NAME + "/" + fileManagementDTO.getRelativePath());
      }
      return fileManagementDTO;
    } catch (Exception e) {
      e.printStackTrace();
      log.info("上传发生错误: {}！", e.getMessage());
    } finally {
      if (fs != null) {
        try {
          fs.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
    Asserts.fail("文件上传失败");
    return null;

  }

  @Override
  public List<FileManagementDTO> saveFiles(MultipartFile[] files, String userId, Long canPreview) {
    List<FileManagementDTO> fileManagementDTOS = new ArrayList<>();
    for (MultipartFile file : files) {
      fileManagementDTOS.add(this.saveFile(file, userId, canPreview));
    }
    return fileManagementDTOS;
  }

  @Override
  public boolean downloadFile2Stream(String fileId, HttpServletResponse response) {
    // 1.根据id查询文件信息
    FileManagementEntity entity = fileManagementDao.findById(fileId).orElse(null);
    if (entity == null) {
      Asserts.fail("文件不存在,请稍后重试");
    }
    try {
      InputStream object = minioClient.getObject(GetObjectArgs.builder().bucket(BUCKET_NAME).object(entity.getRelativePath()).build());
      // 1.设置文件名称
      String fileName = new String(entity.getFileName().getBytes(), "ISO8859-1");
      // 2.设置响应头
      response.setHeader("Content-disposition", "attachment;fileName=" + fileName);
      // 3.设置响应类型
      response.setContentType(FileUtil.getMimeType(entity.getFileName()));
      response.setCharacterEncoding("UTF-8");
      int len;
      while ((len = object.read()) >= 0) {
        response.getOutputStream().write(len);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      log.info("下载发生错误: {}！", e.getMessage());
    }
    return false;
  }




  @Override
  public boolean downloadFiles2Zip(List<String> fileIds, HttpServletResponse response) {
    List<FileManagementEntity> fileManagementEntityList = fileManagementDao.findAllById(fileIds);
    if (fileManagementEntityList.get(0) == null) {
      Asserts.fail("下载列表为空");
    }
    // 设置日期格式
    SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
    String tStamp = format.format(new Date());
    // 设置文件名
    String filename = "Export" + tStamp + ".zip";
    response.setHeader("Content-Disposition", "attachment;fileName=" + filename);
    response.setContentType("application/octet-stream;charset=utf-8");
    try {
      List<String> nameList = new ArrayList<>();
      List<InputStream> inputStreams = new ArrayList<>();
      for (FileManagementEntity entity : fileManagementEntityList) {
        nameList.add(entity.getFileName());
        InputStream object = minioClient.getObject(GetObjectArgs.builder()
                .bucket(BUCKET_NAME)
                .object(entity.getRelativePath())
                .build());
        inputStreams.add(object);
      }
      this.inputStreams2Zip(
              nameList, inputStreams, new ZipOutputStream(response.getOutputStream()));
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      log.info("下载发生错误: {}！", e.getMessage());
    }
    return false;
  }

  private BucketPolicyConfigDto createBucketPolicyConfigDto(String bucketName) {
    BucketPolicyConfigDto.Statement statement =
            BucketPolicyConfigDto.Statement.builder()
                    .Effect("Allow")
                    .Principal("*")
                    .Action("s3:GetObject")
                    .Resource("arn:aws:s3:::" + bucketName + "/*.**")
                    .build();
    return BucketPolicyConfigDto.builder()
            .Version("2012-10-17")
            .Statement(CollUtil.toList(statement))
            .build();
  }

  private void checkMinioStorageBucket() {
    try {
      boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET_NAME).build());
      if (isExist) {
        log.info("存储桶已经存在!");
      } else {
        // 创建存储桶并设置只读权限
        minioClient.makeBucket(MakeBucketArgs.builder().bucket(BUCKET_NAME).build());
        BucketPolicyConfigDto bucketPolicyConfigDto = createBucketPolicyConfigDto(BUCKET_NAME);
        SetBucketPolicyArgs setBucketPolicyArgs =
                SetBucketPolicyArgs.builder()
                        .bucket(BUCKET_NAME)
                        .config(JSONUtil.toJsonStr(bucketPolicyConfigDto))
                        .build();
        minioClient.setBucketPolicy(setBucketPolicyArgs);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private FileManagementDTO saveFileManagement(String file, long size, String userId, Long canPreview) {
    // 1.使用雪花算法创建文件记录表id
    String fileId = IdUtil.createSnowflake(0, 1).nextId()+"";
    // 2.获取文件真实名称
    String fileName = file;
    // 3.获取文件类型
    String fileType = FileUtils.getExtend(fileName);
    // 4.文件大小
    String fileSize = FileUtil.readableFileSize(size);
    // 5.创建存储的相对路径和修改文件名称
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
    String relativePath = sdf.format(new Date()) + "/" + fileId + fileType;
    // 保存文件信息
    FileManagementEntity fileManagementEntity = new FileManagementEntity();
    fileManagementEntity.setFileId(fileId);
    fileManagementEntity.setFileName(fileName);
    fileManagementEntity.setFileType(fileType);
    fileManagementEntity.setFileSize(fileSize);
    fileManagementEntity.setIsNeedPreview(canPreview);
    fileManagementEntity.setRelativePath(relativePath);
    fileManagementEntity.setCreator(userId);
    fileManagementEntity.setCreateTime(new Date());
    return BeanUtil.toBean(fileManagementDao.save(fileManagementEntity), FileManagementDTO.class);
  }

  private void inputStreams2Zip(List<String> nameList, List<InputStream> inputStreams, ZipOutputStream zos) {
    for (int i = 0; i < inputStreams.size(); i++) {
      InputStream inputStream = inputStreams.get(i);
      byte[] buffer = new byte[1024];
      int len;
      ZipEntry entry = new ZipEntry(nameList.get(i));
      try (BufferedInputStream in = new BufferedInputStream(inputStream)) {
        // 获取需要下载的文件流
        zos.putNextEntry(entry);
        // 文件流循环写入ZipOutputStream
        while ((len = in.read(buffer)) != -1) {
          zos.write(buffer, 0, len);
        }
      } catch (Exception e) {
        e.printStackTrace();
      } finally {
        if (entry != null) {
          try {
            zos.closeEntry();
          } catch (Exception e2) {
            e2.printStackTrace();
          }
        }
      }
    }
    // 关闭ZIP流
    try {
      zos.flush();
      zos.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
    // 关闭文件流
    for (InputStream inputStream : inputStreams) {
      if (inputStream != null) {
        try {
          inputStream.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  @Override
  public FileManagementDTO saveVideoFile(MultipartFile file, String userId, Long canPreview) {
    FileInputStream fs = null;
    if (VideoConvertUtil.canConvert(file.getOriginalFilename())) {
      File localTempFile = FileUtils.saveToLocalFile(file);
      if (localTempFile == null) {
        Asserts.fail("文件上传失败");
        return null;
      }
      // 转码很慢，后续优化可以用消息队列
      String filePath = VideoConvertUtil.convertVideoToMp4Format(localTempFile.getPath());
      if (filePath != null) {
        File temp = new File(filePath);
        if (temp.exists()) {
          localTempFile.delete();
          localTempFile = new File(filePath);
        }
      }
      try {
        FileManagementDTO fileManagementDTO =
                this.saveFileManagement(
                        localTempFile.getName(), localTempFile.length(), userId, canPreview);
        fs = new FileInputStream(localTempFile);
        // 使用putObject上传一个文件到存储桶中
        PutObjectArgs putObjectArgs =
                PutObjectArgs.builder()
                        .bucket(BUCKET_NAME)
                        .object(fileManagementDTO.getRelativePath())
                        .contentType(file.getContentType())
                        .stream(fs, localTempFile.length(), ObjectWriteArgs.MIN_MULTIPART_SIZE)
                        .build();
        minioClient.putObject(putObjectArgs);
        fs.close();
        localTempFile.delete();
        return fileManagementDTO;
      } catch (Exception e) {
        e.printStackTrace();
        log.info("上传发生错误: {}！", e.getMessage());
      } finally {
        if (fs != null) {
          try {
            fs.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
      Asserts.fail("文件上传失败");
    } else {
      Asserts.fail("文件后缀名不正确");
    }
    return null;
  }

  @Async("taskExecutor")
  @Override
  public void saveMediaFile(String fileName, InputStream inputStream, String userId, Long canPreview,List<FileManagementDTO> fileDTOs) {
    // 判断是否是媒体文件
    File localTempFile = null;
    File targetFile = null;
    if (VideoConvertUtil.canConvert(fileName)) {
      localTempFile = FileUtils.createTempFile(inputStream, fileName);
      if (localTempFile == null) {
        Asserts.fail("文件上传失败");
        WebSocketServer.sendInfo("文件" + fileName + "上传失败", userId);
      }
      //获取编码属性
      EncodingAttributes attrs = MediaUtil.getEncodingAttributes(fileName.substring(fileName.lastIndexOf(".")+1));
      try {
        //转码器
        Encoder encoder = new Encoder();
        // 防止重名
        long tempFileId = IdUtil.createSnowflake(0, 1).nextId();
        // 临时文件
        targetFile = File.createTempFile("video" + tempFileId, ".mp4");
        MultimediaObject multimediaObject = new MultimediaObject(localTempFile);
        //转码
        encoder.encode(multimediaObject, targetFile, attrs);
        // 保存MP4文件属性到数据库文件记录表
        String newFileName = fileName.substring(0, fileName.lastIndexOf("."));
        FileManagementDTO fileManagementDTO = this.saveFileManagement(newFileName + ".mp4", targetFile.length(), userId, canPreview);
        // 文件输入流
        FileInputStream fs = new FileInputStream(targetFile);
        // 使用putObject上传一个文件到存储桶中
        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .bucket(BUCKET_NAME)
                .object(fileManagementDTO.getRelativePath())
                .stream(fs, targetFile.length(), ObjectWriteArgs.MIN_MULTIPART_SIZE)
                .build();
        minioClient.putObject(putObjectArgs);
        //设置预览地址
        fileManagementDTO.setPreviewPath(ENDPOINT + "/" + BUCKET_NAME + "/" + fileManagementDTO.getRelativePath());
        //返回结果
        Map<String, Object> result = new HashMap<>();
        result.put("fileInfo", fileManagementDTO);
        result.put("message", fileName + "上传并解析成功！");
        //同步时返回值
        fileDTOs.add(fileManagementDTO);
        WebSocketServer.sendInfo(JSONUtil.toJsonStr(result), userId);
        fs.close();
      } catch (Exception e) {
        e.printStackTrace();
        log.info("上传发生错误: {}！", e.getMessage());
        WebSocketServer.sendInfo("文件" + fileName + "上传发生错误", userId);
      } finally {
        if (targetFile != null) {
          targetFile.deleteOnExit();
        }
        localTempFile.deleteOnExit();
      }
    } else {
      Asserts.fail("文件格式不正确");
      WebSocketServer.sendInfo("文件" + fileName + "文件格式不正确", userId);
    }
  }

  @Override
  public List<FileManagementDTO> readZipFile(MultipartFile file, String userId) {
      List<FileManagementDTO> fileDTOs = new ArrayList<>();
      Long tempId = IdUtil.createSnowflake(0,1).nextId();
      File tempFile = null;
      try {
          tempFile = File.createTempFile("zip"+tempId,".zip");
          file.transferTo(tempFile);
          InputStream in = new FileInputStream(tempFile);
          //这里编码都得相同否则后面会找不到文件
          ZipFile zipFile = new ZipFile(tempFile,Charset.forName("ISO8859-1"));
          ZipInputStream zin  = new ZipInputStream(in,Charset.forName("ISO8859-1"));
          ZipEntry ze;
          while((ze = zin.getNextEntry())!=null){
              if(!ze.isDirectory()){
                  //判断是否是可预览类型
                  Long canPreview = FileManagementConstant.NO_NEED_PREVIEW;
                  if(FileUtils.canPreView(ze.getName())){
                      canPreview = FileManagementConstant.NEED_PREVIEW;
                  }
                  //上传
                  InputStream ein = zipFile.getInputStream(ze);
                  //获取MIME类型
                  FileNameMap fileNameMap = URLConnection.getFileNameMap();
                  String contentType = fileNameMap.getContentTypeFor(ze.getName());
                  String fileName = ze.getName().substring(ze.getName().lastIndexOf("/")+1);
                  //判断是不是视频类型
                  if(VideoConvertUtil.canConvert(ze.getName())){
                      this.saveMediaFile(fileName,ein,userId,canPreview,fileDTOs);
                  }else {
                      //普通文件上传
                      fileDTOs.add(this.saveFile(fileName, ze.getSize(), contentType, ein, userId, canPreview));
                  }
              }
              zin.closeEntry();
          }
          zin.close();
        return fileDTOs;
      } catch (IOException e) {
          e.printStackTrace();
      } finally {
          tempFile.deleteOnExit();
      }
      return null;
  }



}